// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Linq;
using JetBrains.Annotations;

namespace SnapObjects.Data
{
    internal class ModelUpdateInfo
    {
        private readonly Func<bool, string> _generateParameterName;
        private readonly IUpdatableModel _updatableModel;
        private readonly IReadOnlyList<ColumnModification> _columnModifications;
        private IReadOnlyDictionary<IAdoDbParameter, bool> _outputParameters;

        public ModelUpdateInfo(
            [NotNull] string table,
            [CanBeNull] string schema,
            [NotNull] IUpdatableModel updatableModel,
            [NotNull] Func<bool, string> generateParameterName)
        {
            this.Table = updatableModel.UppercaseIdentifier ? table?.ToUpper() : table;
            this.Schema = updatableModel.UppercaseIdentifier ? schema?.ToUpper() : schema;

            _updatableModel = updatableModel;
            _generateParameterName = generateParameterName;

            this.ModelState = updatableModel.ModelState;
            _columnModifications = this.BuildColumnModifications();
        }

        public virtual string Table { get; }

        public virtual string Schema { get; }

        public virtual ModelState ModelState { get; }

        public virtual bool HasWrite { get; private set; }

        internal virtual IReadOnlyList<ColumnModification> ColumnModifications => _columnModifications;

        internal virtual IReadOnlyDictionary<IAdoDbParameter, bool> OutputParameters => _outputParameters;

        internal IReadOnlyDictionary<IAdoDbParameter, bool> BuildOutputParameters()
        {
            var outputParameters = new Dictionary<IAdoDbParameter, bool>();

            var outputParameterName = "";

            if (_updatableModel.ModelState == ModelState.Deleted)
            {
                outputParameterName = _generateParameterName(true);
            }
            else if (_updatableModel.ModelState == ModelState.Modified)
            {
                outputParameterName = _generateParameterName(true);
            }
            else if (_updatableModel.ModelState == ModelState.NewModified)
            {
                var columnModifications = this.ColumnModifications.Where(m => m.IsRead);

                foreach (var columnModification in columnModifications)
                {
                    var parameter = new AdoDbParameter(columnModification.ParameterName,
                        null,
                        columnModification.Property.DataType,
                        ParameterDirection.Output);

                    if (columnModification.Property.HasConversion)
                    {
                        parameter.ValueConverter = columnModification.Property.ValueConverter;
                    }

                    outputParameters.Add(parameter, true);
                }
            }

            if (!String.IsNullOrEmpty(outputParameterName))
            {
                var parameter = new AdoDbParameter(outputParameterName,
                    null,
                    typeof(int),
                    ParameterDirection.Output);

                outputParameters.Add(parameter, false);
            }

            _outputParameters = outputParameters;

            return _outputParameters;
        }

        private IReadOnlyList<ColumnModification> BuildColumnModifications()
        {
            var columnModifications = new List<ColumnModification>();

            var modelMeta = _updatableModel.ModelMeta;
            var properties = modelMeta.UpdatableProperties;

            this.CheckKeyForUpdateWhereStrategy(properties);

            foreach (var propertyMeta in properties)
            {
                var isInsert = _updatableModel.ModelState == ModelState.New ||
                    _updatableModel.ModelState == ModelState.NewModified;

                var columnModification = new ColumnModification(
                    _updatableModel,
                    propertyMeta,
                    _generateParameterName,
                    this.IsReadValue(propertyMeta),
                    this.IsWriteValue(propertyMeta, isInsert),
                    propertyMeta.IsKey,
                    propertyMeta.IsIdentity,
                    this.IncludeInUpdateWhere(propertyMeta),
                    propertyMeta.IsConcurrencyCheck,
                    this.UseOriginalValueParameter(propertyMeta),
                    this.UseCurrentValueParameter(propertyMeta, isInsert));

                columnModifications.Add(columnModification);

                if (columnModification.IsWrite)
                {
                    this.HasWrite = true;
                }
            }

            return columnModifications;
        }

        private void CheckKeyForUpdateWhereStrategy(IEnumerable<IUpdatableProperty> properties)
        {
            if ((_updatableModel.ModelState == ModelState.Deleted ||
                _updatableModel.ModelState == ModelState.Modified) &&
                !properties.Any(m => m.IsKey))
            {
                throw new InvalidOperationException("No Key(s) defined in model.");
            }
        }

        private bool IncludeInUpdateWhere(IUpdatableProperty property)
        {
            if (property.IsNotInWhere)
            {
                return false;
            }

            if (_updatableModel.ModelState == ModelState.New ||
                _updatableModel.ModelState == ModelState.NewModified)
            {
                return false;
            }

            if (_updatableModel.UpdateWhereStrategy == UpdateWhereStrategy.KeyAndConcurrencyCheckColumns)
            {
                return property.IsKey ||
                    property.IsConcurrencyCheck ||
                    property.IsTimestamp;
            }

            if (_updatableModel.UpdateWhereStrategy == UpdateWhereStrategy.KeyAndModifiedColumns)
            {
                return property.IsKey ||
                    _updatableModel.IsModified(property) ||
                    property.IsTimestamp;
            }

            return property.IsKey || property.IsTimestamp;
        }

        private bool IsReadValue(IUpdatableProperty property)
        {
            bool isDbGenerated;

            if (_updatableModel.ModelState == ModelState.Deleted)
            {
                isDbGenerated = false;
            }
            else if (_updatableModel.ModelState == ModelState.Modified)
            {
                isDbGenerated = property.IsTimestamp ||
                    property.SaveStrategy == SaveStrategy.ReadAfterSave ||
                    (property is IPocoProperty p &&
                    p.Attribute.DatabaseGeneratedDefined &&
                    p.Attribute.DatabaseGenerated.DatabaseGeneratedOption == DatabaseGeneratedOption.Computed);
            }
            else
            {
                isDbGenerated = this.IsDatabaseGenerated(property);
            }

            return isDbGenerated;
        }

        private bool IsDatabaseGenerated(IUpdatableProperty property)
        {
            var isDataBaseGenerated = property.IsIdentity ||
                property.IsTimestamp ||
                property.SaveStrategy == SaveStrategy.ReadAfterSave;

            if (!isDataBaseGenerated && property is IPocoProperty p)
            {
                isDataBaseGenerated = p.Attribute.DatabaseGeneratedDefined &&
                    p.Attribute.DatabaseGenerated.DatabaseGeneratedOption != DatabaseGeneratedOption.None;

                if (!isDataBaseGenerated)
                {
                    isDataBaseGenerated = p.Attribute.SqlDefaultValueDefined &&
                        !_updatableModel.IsModified(property);
                }
            }

            return isDataBaseGenerated;
        }

        private bool IsWriteValue(IUpdatableProperty property, bool isInsert)
        {
            if (_updatableModel.ModelState == ModelState.Deleted)
            {
                return false;
            }

            var saveStrategy = property.SaveStrategy;

            var isDatabaseGenerated = this.IsDatabaseGenerated(property);

            var canSave = saveStrategy == SaveStrategy.Save
                          || (this.ModelState == ModelState.NewModified && saveStrategy == SaveStrategy.SaveOnCreate)
                          || (this.ModelState == ModelState.Modified && saveStrategy == SaveStrategy.SaveOnUpdate);

            //SqlDefaultValueDefined, if modified need to write, or need to read.
            var isWrite = !isDatabaseGenerated && canSave;

            if (isWrite)
            {
                isWrite = _updatableModel.IsModified(property);

                if (!isWrite &&
                    isInsert &&
                    !ModelHelper.IsDynamicModel(_updatableModel.ModelMeta.ModelType))
                {
                    isWrite = true;
                }
            }

            return isWrite;
        }

        private bool UseOriginalValueParameter(IUpdatableProperty property)
        {
            //EF: IsCondition && IsConcurrencyToken
            return this.IncludeInUpdateWhere(property);
        }

        private bool UseCurrentValueParameter(IUpdatableProperty property, bool isInsert)
        {
            //EF: IsWrite || IsCondition && !IsConcurrencyToken
            return this.IsWriteValue(property, isInsert);
        }
    }
}
